 /*******************************************************************************
  * Copyright (c) 2000, 2004 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Common Public License v1.0
  * which accompanies this distribution, and is available at
  * http://www.eclipse.org/legal/cpl-v10.html
  *
  * Contributors:
  * IBM Corporation - initial API and implementation
  *******************************************************************************/

 package org.eclipse.core.internal.registry;

 import java.io.*;
 import java.util.ArrayList ;
 import java.util.List ;
 import org.eclipse.core.internal.runtime.InternalPlatform;
 import org.eclipse.core.internal.runtime.Policy;
 import org.eclipse.core.runtime.*;

 /**
  * Failure reporting strategy:
  * - when reading an identified element (bundles, extension points and extensions),
  * catch any IOExceptions and rethrow them wrapped into a InvalidRegistryCacheException
  * that describes where the error happened
  * - IOExceptions while reading non-identified elements (configuration elements and properties)
  * are just propagated to their callers
  * - the public entry points will catch any exceptions to ensure they are logged and return a valid
  * value instead (for instance, a null reference or an empty array)
  */
 public class RegistryCacheReader {

     MultiStatus problems = null;
     // objectTable will be an array list of objects. The objects will be things
 // like a plugin descriptor, extension point, etc. The integer
 // index value will be used in the cache to allow cross-references in the
 // cached registry.
 protected List objectTable = null;
     private boolean lazilyLoadExtensions;
     private boolean flushableExtensions = true;
     // indicates we failed load configuration elements lazily
 private boolean failed;
     protected File cacheFile;

     public static final byte REGISTRY_CACHE_VERSION = 7;
     public static final byte NULL = 0;
     public static final byte OBJECT = 1;
     public static final byte INDEX = 2;

     public RegistryCacheReader(File cacheFile, MultiStatus problems, boolean lazilyLoadExtensions, boolean flushable) {
         super();
         this.cacheFile = cacheFile;
         this.problems = problems;
         this.lazilyLoadExtensions = lazilyLoadExtensions;
         this.flushableExtensions = flushable;
         objectTable = new ArrayList ();
     }

     public RegistryCacheReader(File cacheFile, MultiStatus problems) {
         this(cacheFile, problems, false, true);
     }

     private int addToObjectTable(Object object) {
         objectTable.add(object);
         // return the index of the object just added (i.e. size - 1)
 return (objectTable.size() - 1);
     }

     private void debug(String msg) {
         System.out.println("RegistryCacheReader: " + msg); //$NON-NLS-1$
 }

     private boolean readHeaderInformation(DataInputStream in, long expectedTimestamp) throws InvalidRegistryCacheException {
         try {
             if (in.readInt() != REGISTRY_CACHE_VERSION)
                 return false;
             long installStamp = in.readLong();
             long registryStamp = in.readLong();
             String osStamp = in.readUTF();
             String windowsStamp = in.readUTF();
             String localeStamp = in.readUTF();
             InternalPlatform info = InternalPlatform.getDefault();
             return ((expectedTimestamp == 0 || expectedTimestamp == registryStamp) && (installStamp == info.getStateTimeStamp()) && (osStamp.equals(info.getOS())) && (windowsStamp.equals(info.getWS())) && (localeStamp.equals(info.getNL())));
         } catch (IOException e) {
             throw new InvalidRegistryCacheException(Policy.bind("meta.regCacheIOExceptionReading", "HeaderInformation"), e); //$NON-NLS-1$//$NON-NLS-2$
 }
     }

     private void skipConfigurationElement(RegistryModelObject parent, DataInputStream in) throws IOException {
         readCachedString(in, false); //read name
 skipString(in); //skip value

         int length = in.readInt();
         for (int i = 0; i < length; i++) {
             skipConfigurationProperty(in);
         }
         length = in.readInt();
         for (int i = 0; i < length; i++) {
             skipConfigurationElement(null, in);
         }
     }

     private ConfigurationElement readConfigurationElement(RegistryModelObject parent, DataInputStream in) throws IOException {
         ConfigurationElement result = new ConfigurationElement();
         result.setParent(parent);
         result.setName(readCachedString(in, false));
         result.setValue(readString(in, false));

         int length = in.readInt();
         ConfigurationProperty[] properties = new ConfigurationProperty[length];
         for (int i = 0; i < length; i++) {
             properties[i] = readConfigurationProperty(in);
         }
         result.setProperties(properties);

         length = in.readInt();
         IConfigurationElement[] elements = new ConfigurationElement[length];
         for (int i = 0; i < length; i++) {
             elements[i] = readConfigurationElement(result, in);
         }
         result.setChildren(elements);
         return result;
     }

     private void skipConfigurationProperty(DataInputStream in) throws IOException {
         readCachedString(in, false); //Read the name
 skipString(in); // skip the value
 }

     private ConfigurationProperty readConfigurationProperty(DataInputStream in) throws IOException {
         String name = readCachedString(in, false);
         ConfigurationProperty result = new ConfigurationProperty();
         result.setName(name);
         result.setValue(readString(in, false));
         return result;
     }

     private Extension readExtension(DataInputStream in) throws InvalidRegistryCacheException {
         Extension result = null;
         try {
             result = (Extension) readIndex(in);
             if (result != null)
                 return result;
             result = flushableExtensions ? new FlushableExtension() : new Extension();
             addToObjectTable(result);
             result.setSimpleIdentifier(readString(in, false));
             result.setParent(readBundleModel(in));
             result.setName(readString(in, false));
             result.setExtensionPointIdentifier(readCachedString(in, false));
             result.setSubElements(readSubElements(result, in));
             return result;
         } catch (IOException e) {
             String extensionId = null;
             if (result != null && result.getParent() != null)
                 extensionId = result.getParentIdentifier() + "." + result.getSimpleIdentifier(); //$NON-NLS-1$
 throw new InvalidRegistryCacheException(Policy.bind("meta.regCacheIOExceptionReading", "extension: " + extensionId), e); //$NON-NLS-1$ //$NON-NLS-2$
 }
     }

     private ExtensionPoint readExtensionPoint(Namespace bundle, DataInputStream in) throws InvalidRegistryCacheException {
         ExtensionPoint result = null;
         try {
             result = (ExtensionPoint) readIndex(in);
             if (result != null)
                 return result;
             result = new ExtensionPoint();
             addToObjectTable(result);
             result.setParent(bundle);
             result.setSimpleIdentifier(readString(in, true));
             result.setName(readString(in, false));
             result.setSchema(readString(in, false));

             // Now do the extensions.
 int length = in.readInt();
             IExtension[] extensions = new Extension[length];
             for (int i = 0; i < length; i++)
                 extensions[i] = readExtension(in);
             result.setExtensions(extensions);
             return result;
         } catch (IOException e) {
             String extensionPointId = null;
             if (result != null && result.getParent() != null)
                 extensionPointId = result.getUniqueIdentifier();
             throw new InvalidRegistryCacheException(Policy.bind("meta.regCacheIOExceptionReading", "extension point: " + extensionPointId), e); //$NON-NLS-1$ //$NON-NLS-2$
 }
     }

     private Namespace readBundleModel(DataInputStream in) throws InvalidRegistryCacheException {
         Namespace result = null;
         try {
             result = (Namespace) readIndex(in);
             if (result != null)
                 return result;
             result = new Namespace();
             addToObjectTable(result);
             result.setUniqueIdentifier(readCachedString(in, true));
             result.setBundle(InternalPlatform.getDefault().getBundleContext().getBundle(in.readLong()));
             result.setParent(readRegistry(in));
             result.setHostIdentifier(readCachedString(in, false));

             // now do extension points
 int length = in.readInt();
             IExtensionPoint[] extensionPoints = new ExtensionPoint[length];
             for (int i = 0; i < length; i++)
                 extensionPoints[i] = readExtensionPoint(result, in);
             result.setExtensionPoints(extensionPoints);

             // and then extensions
 length = in.readInt();
             IExtension[] extensions = flushableExtensions ? new FlushableExtension[length] : new Extension[length];
             for (int i = 0; i < length; i++)
                 extensions[i] = readExtension(in);
             result.setExtensions(extensions);
             return result;
         } catch (IOException e) {
             String bundleId = (result == null || result.getUniqueIdentifier() == null) ? "<not available>" : result.getUniqueIdentifier(); //$NON-NLS-1$
 throw new InvalidRegistryCacheException(Policy.bind("meta.regCacheIOExceptionReading", "plugin: " + bundleId), e); //$NON-NLS-1$//$NON-NLS-2$
 }
     }

     private ExtensionRegistry readCache(DataInputStream in, long expectedTimestamps) throws InvalidRegistryCacheException {
         if (!readHeaderInformation(in, expectedTimestamps)) {
             if (InternalPlatform.DEBUG_REGISTRY)
                 debug("Cache header information out of date - ignoring cache"); //$NON-NLS-1$
 return null;
         }
         return readRegistry(in);
     }

     private ExtensionRegistry readRegistry(DataInputStream in) throws InvalidRegistryCacheException {
         try {
             ExtensionRegistry result = (ExtensionRegistry) readIndex(in);
             if (result != null)
                 return result;

             result = new ExtensionRegistry();
             if (lazilyLoadExtensions)
                 result.setCacheReader(this);
             addToObjectTable(result);
             // if there are no plugins in the registry, return null instead of
 // an empty registry?
 int length = in.readInt();
             if (length == 0)
                 return null;
             for (int i = 0; i < length; i++)
                 result.basicAdd(readBundleModel(in), false);
             if (lazilyLoadExtensions)
                 result.setCacheReader(this);
             return result;
         } catch (IOException e) {
             throw new InvalidRegistryCacheException(Policy.bind("meta.regCacheIOExceptionReading", "ExtensionRegistry"), e); //$NON-NLS-1$//$NON-NLS-2$
 }
     }

     private ConfigurationElement[] readSubElements(Extension parent, DataInputStream in) throws IOException {
         int type = in.readByte();
         if (type == NULL)
             return null;

         //Here type is OBJECT
 // the first field is extension sub-elements data offset
 int offset = in.readInt();

         if (lazilyLoadExtensions) {
             Extension extension = parent;
             extension.setSubElementsCacheOffset(offset);
             skipBasicSubElements(parent, in);
             extension.setFullyLoaded(false);
             return null;
         }
         return readBasicSubElements(parent, in);
     }

     private void skipBasicSubElements(Extension parent, DataInputStream in) throws IOException {
         int length = in.readInt();
         for (int i = 0; i < length; i++) {
             skipConfigurationElement(parent, in);
         }
     }

     private ConfigurationElement[] readBasicSubElements(Extension parent, DataInputStream in) throws IOException {
         // read the number of sub elements to load
 int length = in.readInt();

         ConfigurationElement[] result = new ConfigurationElement[length];
         for (int i = 0; i < length; i++) {
             result[i] = readConfigurationElement(parent, in);
         }
         return result;
     }

     private String readString(DataInputStream in, boolean intern) throws IOException {
         byte type = in.readByte();
         if (type == NULL)
             return null;
         if (intern)
             return in.readUTF().intern();
         else
             return in.readUTF();
     }

     private void skipString(DataInputStream in) throws IOException {
         byte type = in.readByte();
         if (type == NULL)
             return;
         int utfLength = in.readUnsignedShort();
         byte bytearr[] = new byte[utfLength];
         in.readFully(bytearr, 0, utfLength);
     }

     private String readCachedString(DataInputStream in, boolean intern) throws IOException {
         byte type = in.readByte();
         if (type == NULL)
             return null;

         if (type == INDEX)
             return (String ) objectTable.get(in.readInt());

         String stringRead = null;
         if (intern)
             stringRead = in.readUTF().intern();
         else
             stringRead = in.readUTF();
         addToObjectTable(stringRead);
         return stringRead;
     }

     private Object readIndex(DataInputStream in) throws IOException {
         byte type = in.readByte();
         return type == INDEX ? objectTable.get(in.readInt()) : null;
     }

     private DataInputStream openCacheFile() throws IOException {
         return new DataInputStream(new BufferedInputStream(new FileInputStream(cacheFile), 2048));
     }

     /**
      * Lazily loads an extension model's sub-elements.
      */
     public final ConfigurationElement[] loadConfigurationElements(Extension parent, int offset) {
         DataInputStream in = null;
         try {
             in = openCacheFile();
             in.skipBytes(offset);
             in.readInt(); // skip the offset itself
 return readBasicSubElements(parent, in);
         } catch (IOException e) {
             Throwable exception = InternalPlatform.DEBUG_REGISTRY ? e : null;
             String message = Policy.bind("meta.unableToReadCache"); //$NON-NLS-1$
 InternalPlatform.getDefault().log(new Status(IStatus.WARNING, Platform.PI_RUNTIME, 0, message, exception));
         } catch (OutOfMemoryError oome) {
             // catch any OutOfMemoryErrors that may have been caused by corrupted data
 logError(oome);
         } catch (RuntimeException re) {
             // catch any ArrayIndexOutOfBounds/NullPointer/NegativeArraySize/... exceptions that may have been caused by corrupted data
 logError(re);
         } finally {
             try {
                 if (in != null)
                     in.close();
             } catch (IOException e) {
                 // ignore
 }
         }
         // we only get here when we have problems
 failed = true;
         return new ConfigurationElement[0];
     }

     boolean hasFailed() {
         return failed;
     }

     private void logError(Throwable t) {
         // log general message
 String message = Policy.bind("meta.registryCacheReadProblems"); //$NON-NLS-1$
 InternalPlatform.getDefault().log(new Status(IStatus.WARNING, Platform.PI_RUNTIME, 0, message, null));
         // log actual error
 Throwable exceptionToLog = InternalPlatform.DEBUG_REGISTRY ? t : null;
         InternalPlatform.getDefault().log(new Status(IStatus.WARNING, Platform.PI_RUNTIME, 0, t.toString(), exceptionToLog));
     }

     public final ExtensionRegistry loadCache() {
         return this.loadCache(0);
     }

     /*
      * If expectedTimestamp != 0, check it against the registry timestamp if the header.
      */
     public final ExtensionRegistry loadCache(long expectedTimestamp) {
         DataInputStream in = null;
         try {
             in = openCacheFile();
         } catch (IOException e) {
             Throwable exception = InternalPlatform.DEBUG_REGISTRY ? e : null;
             String message = Policy.bind("meta.unableToReadCache"); //$NON-NLS-1$
 InternalPlatform.getDefault().log(new Status(IStatus.WARNING, Platform.PI_RUNTIME, 0, message, exception));
             return null;
         }
         try {
             return readCache(in, expectedTimestamp);
         } catch (InvalidRegistryCacheException e) {
             Throwable exception = InternalPlatform.DEBUG_REGISTRY ? e.getCause() : null;
             InternalPlatform.getDefault().log(new Status(IStatus.WARNING, Platform.PI_RUNTIME, 0, e.getMessage(), exception));
         } catch (OutOfMemoryError oome) {
             // catch any OutOfMemoryErrors that may have been caused by corrupted data
 logError(oome);
         } catch (RuntimeException re) {
             // catch any ArrayIndexOutOfBounds/NullPointer/NegativeArraySize/... exceptions that may have been caused by corrupted data
 logError(re);
         } finally {
             try {
                 if (in != null)
                     in.close();
             } catch (IOException e) {
                 // ignore
 }
         }
         return null;
     }

     public class InvalidRegistryCacheException extends Exception {
         Throwable cause = null;

         public InvalidRegistryCacheException(String msg, Throwable cause) {
             super(msg);
             this.cause = cause;
         }

         public InvalidRegistryCacheException(String string) {
             super(string);
         }

         public Throwable getCause() {
             return cause;
         }
     }
 }
